geolocation: add GeoProbe state and CRUD instructions#3120
geolocation: add GeoProbe state and CRUD instructions#3120
Conversation
| return Err(GeolocationError::InvalidServiceabilityProgramId.into()); | ||
| } | ||
|
|
||
| // Verify it's a valid, activated Exchange account |
There was a problem hiding this comment.
FYI to reviewers: in RFC16 there's a to-do item to implement cross-program reference counting. We'll do it in a future PR to keep this PR more manageable.
27afe8a to
df46177
Compare
Add GeoProbe account type with CreateGeoProbe, UpdateGeoProbe, and DeleteGeoProbe instructions. Includes IP validation (rejects all non-publicly-routable addresses), code length validation, exchange activation checks, and reference count protection on delete. Reorder GeoProbe struct fields to place variable-length fields (code, parent_devices) at the end for correct Borsh deserialization. Update RFC16 to match and remove latency_threshold_ns. Part 2 of 3 for RFC16 geolocation verification.
- Add comprehensive integration tests for create, update, and delete GeoProbe operations - Test validation logic for IP addresses, code length, and exchange status - Test reference count protection on deletion - Update CLAUDE.md to require integration tests for all processors - Remove unused system_program parameter from update processor
df46177 to
1dfbf75
Compare
…me environment where the program upgrade authority check expects the payer to match the authority stored in the program data account
1dfbf75 to
083f58f
Compare
| if self.code.len() > 32 { | ||
| msg!("Code too long: {} bytes", self.code.len()); | ||
| return Err(GeolocationError::InvalidCodeLength); | ||
| } |
There was a problem hiding this comment.
This would never be validated, would it? In the create instruction, you validate the code length, so I can't imagine a GeoProbe would ever have a code > 32.
Plus find_program_address would have borked if this were >32 anyway, so this feels impossible given these two reasons
| if self.parent_devices.len() > MAX_PARENT_DEVICES { | ||
| msg!( | ||
| "Too many parent devices: {} (max {})", | ||
| self.parent_devices.len(), | ||
| MAX_PARENT_DEVICES | ||
| ); | ||
| return Err(GeolocationError::MaxParentDevicesReached); | ||
| } |
There was a problem hiding this comment.
No instructions add parent devices yet. But I'm wondering if this would ever hit, too? I'm guessing when an instruction that introduces adding parent devices, there would be validation of whether this violates MAX_PARENT_DEVICES, so this condition would never be true.
Curious about a scenario where this would be true.
| impl Validate for GeoProbe { | ||
| fn validate(&self) -> Result<(), GeolocationError> { | ||
| if self.account_type != AccountType::GeoProbe { | ||
| msg!("Invalid account type: {}", self.account_type); | ||
| return Err(GeolocationError::InvalidAccountType); | ||
| } | ||
| if self.code.len() > 32 { | ||
| msg!("Code too long: {} bytes", self.code.len()); | ||
| return Err(GeolocationError::InvalidCodeLength); | ||
| } | ||
| if self.parent_devices.len() > MAX_PARENT_DEVICES { | ||
| msg!( | ||
| "Too many parent devices: {} (max {})", | ||
| self.parent_devices.len(), | ||
| MAX_PARENT_DEVICES | ||
| ); | ||
| return Err(GeolocationError::MaxParentDevicesReached); | ||
| } | ||
| Ok(()) | ||
| } | ||
| } |
There was a problem hiding this comment.
Now that I've written the two comments above, I'm wondering why this trait even needs to be implemented with any of the accounts you have in this smart contract. What does calling validate protect against exactly?
| #[test] | ||
| fn test_program_config_pda_is_deterministic() { | ||
| let program_id = test_program_id(); | ||
| let (pda1, bump1) = get_program_config_pda(&program_id); | ||
| let (pda2, bump2) = get_program_config_pda(&program_id); | ||
| assert_eq!(pda1, pda2); | ||
| assert_eq!(bump1, bump2); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_program_config_pda_differs_by_program_id() { | ||
| let (pda1, _) = get_program_config_pda(&Pubkey::new_unique()); | ||
| let (pda2, _) = get_program_config_pda(&Pubkey::new_unique()); | ||
| assert_ne!(pda1, pda2); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_geo_probe_pda_is_deterministic() { | ||
| let program_id = test_program_id(); | ||
| let (pda1, bump1) = get_geo_probe_pda(&program_id, "probe-a"); | ||
| let (pda2, bump2) = get_geo_probe_pda(&program_id, "probe-a"); | ||
| assert_eq!(pda1, pda2); | ||
| assert_eq!(bump1, bump2); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_geo_probe_pda_differs_by_code() { | ||
| let program_id = test_program_id(); | ||
| let (pda1, _) = get_geo_probe_pda(&program_id, "probe-a"); | ||
| let (pda2, _) = get_geo_probe_pda(&program_id, "probe-b"); | ||
| assert_ne!(pda1, pda2); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_geo_probe_pda_differs_by_program_id() { | ||
| let (pda1, _) = get_geo_probe_pda(&Pubkey::new_unique(), "probe-a"); | ||
| let (pda2, _) = get_geo_probe_pda(&Pubkey::new_unique(), "probe-a"); | ||
| assert_ne!(pda1, pda2); | ||
| } |
There was a problem hiding this comment.
These tests don't seem necessary (and would argue the one below this comment isn't either, too). What do you think?
| let probe = GeoProbe::try_from(&probe_account.data[..]).unwrap(); | ||
|
|
||
| assert_eq!(probe.account_type, AccountType::GeoProbe); | ||
| assert_eq!(probe.owner, payer.pubkey()); | ||
| assert_eq!(probe.exchange_pk, exchange_pubkey); | ||
| assert_eq!(probe.public_ip, Ipv4Addr::new(8, 8, 8, 8)); | ||
| assert_eq!(probe.location_offset_port, 4242); | ||
| assert_eq!(probe.metrics_publisher_pk, args.metrics_publisher_pk); | ||
| assert_eq!(probe.reference_count, 0); | ||
| assert_eq!(probe.code, code); | ||
| assert_eq!(probe.parent_devices.len(), 0); |
There was a problem hiding this comment.
FYI, if GeoProbe derives PartialEq then it would be better to assert_eq!(probe, expected_probe) in case you add more fields to your geo probe account in the future. With the way this is written, you run the risk of missing asserting that the new fields will be equal what you expect
| ); | ||
|
|
||
| let result = banks_client.process_transaction(tx).await; | ||
| assert!(result.is_err()); |
There was a problem hiding this comment.
Does not seem necessary since you unwrap the error later on. Should the claude markdown be informed to not perform this is_err check?
| assert!(result.is_err()); | ||
| // Exchange not activated should return InvalidAccountData |
There was a problem hiding this comment.
Do you want to assert that the error equals something specific?
| // Verify immutable fields didn't change | ||
| assert_eq!(probe.code, code); | ||
| assert_eq!(probe.exchange_pk, exchange_pubkey); |
There was a problem hiding this comment.
I like the assertion that immutable fields don't change. But like the comment above, might be better to assert full struct equality in case more immutable fields get added
|
|
||
| 1. **Assert specific errors**: Tests should assert specific error types (e.g., `ProgramError::Custom(17)`), not just `.is_err()`. This catches regressions where the instruction fails for the wrong reason. | ||
|
|
||
| 2. **Don't test framework functionality**: Avoid writing tests that only exercise SDK/framework behavior (e.g., testing that `Pubkey::find_program_address` is deterministic or produces different outputs for different inputs). Focus tests on your program's logic. |
There was a problem hiding this comment.
I guess when the unit tests were generated in this PR, claude forgot about this
|
|
||
| ### Testing | ||
|
|
||
| 1. **Assert specific errors**: Tests should assert specific error types (e.g., `ProgramError::Custom(17)`), not just `.is_err()`. This catches regressions where the instruction fails for the wrong reason. |
Summary of Changes
GeoProbeaccount type to the Geolocation Program withCreateGeoProbe,UpdateGeoProbe, andDeleteGeoProbeinstructions (foundation-gated via Serviceability CPI)reference_count == 0["doublezero", "probe", code.as_bytes()]) and extend serializer withtry_acc_closefor account deletionDiff Breakdown
Testing Verification